/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.servlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vacoor.nothing.common.util.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * 实现自动检测 GET 请求参数编码并解码, 不支持 POST URL 中带 GET 参数eg: POST xxx?name=23
 * <p/>
 * 需要指定容器编码:
 * 对于 Tomcat 而言, 可以配置 server.xml <Connector URIEncoding="UTF-8" ... />
 * <p/>
 * 注:
 * IE 直接GET请求使用GB编码, Chrome, FF, Safari 使用 UTF-8
 * IE8 直接GET 不编码(??)
 *
 * @author vacoor
 */
public class HttpGetEncodingFilter implements Filter {
    private static final Logger LOG = LoggerFactory.getLogger(HttpGetEncodingFilter.class);

    public static final String CONTAINER_URI_ENCODING_NAME = "containerURIEncoding";
    private static final String DEFAULT_CONTAINER_URI_ENCODING = "ISO-8859-1";

    private String containerURIEncoding = DEFAULT_CONTAINER_URI_ENCODING;
    private static final Charset[] charsets = {Charset.forName("UTF-8"), Charset.forName("GBK")};


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String qs = request.getQueryString();
        String method = request.getMethod();

        if ("GET".equals(method) && StringUtils.hasText(qs)) {
            int len = qs.length();
            byte[] bytes = new byte[len];
            int offset = 0;
            boolean encoded = true;

            for (int i = 0; i < len; i++) {
                char c = qs.charAt(i);

                if (c >= 0x80) {    // 如果存在非 US-ASCII 则说明URL没有编码
                    encoded = false;
                    offset = 0;
                    break;
                } else if (';' == c) {
                    break;
                } else if ('%' == c && (i + 2) < len) {
                    bytes[offset++] = (byte) Integer.parseInt(qs.substring(i + 1, i + 3), 16);
                    i += 2;
                }
            }

            bytes = !encoded ? qs.getBytes(containerURIEncoding) : Arrays.copyOf(bytes, offset);

            if (0 < bytes.length) {
                Charset charset = detectedCharset(bytes, charsets);
                // 如果无法获取或与服务器编码一致
                if (charset != null && !charset.name().equalsIgnoreCase(containerURIEncoding)) {
                    request = new GetRequestDecodingWrapper(request, charset);
                }
            }
        }
        chain.doFilter(request, servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String enc = filterConfig.getInitParameter(CONTAINER_URI_ENCODING_NAME);
        if (enc != null) {
            containerURIEncoding = enc;
        }
        LOG.debug("init container uri encoding: {}", containerURIEncoding);
    }

    @Override
    public void destroy() {

    }

    /**
     * 检测给定字符串的字节数组的字符编码
     * 如果字符编码列表中不存在能正确解析字节数组的字符编码, 将返回 null
     * <p/>
     * 注意: 对于多个字符编码可能相同的字节数组会对应不同的字符, 将按照给定顺序优先匹配
     * eg: GBK 中文 -- Big5 笢; GBK 文 -- Big5 恅 字节是一样的
     * 某个字符在多种编码中占用长度一样时, 该情况比较多
     *
     * @param bytes    字符串的字节数组
     * @param charsets 字符编码列表
     * @return
     */
    private static Charset detectedCharset(byte[] bytes, Charset[] charsets) {
        for (Charset charset : charsets) {
            if (canDecode(bytes, charset)) {
                return charset;
            }
        }
        return null;
    }

    private static boolean canDecode(byte[] bytes, Charset charset) {
        if (charset == null) {
            throw new IllegalArgumentException("charsets must not be null");
        }

        /**
         * {@link String#getBytes()} 始终是返回这个字符串给定编码的字节数组
         * 如果使用特定字符编码解析字节数组后字符串为乱码, 则 getBytes 获取的是这个乱码的字节数组
         * 也就是说一旦一个字节数组被转换为字符串就无法通过这个字符串再获取原来的字节数组
         */
        byte[] bytes2 = new String(bytes, charset).getBytes(charset);

        return Arrays.equals(bytes, bytes2);
    }


    /**
     *
     */
    private class GetRequestDecodingWrapper extends HttpServletRequestWrapper {
        private final Charset charset;
        private final Map<String, String[]> params;

        @SuppressWarnings("unchecked")
        public GetRequestDecodingWrapper(HttpServletRequest request, Charset charset) {
            super(request);
            this.charset = charset;
            params = decodeParameter(request.getParameterMap());
        }

        @Override
        public String getQueryString() {
            String queryString = ((HttpServletRequest) getRequest()).getQueryString();
            try {
                queryString = URLDecoder.decode(queryString, charset.name());
            } catch (UnsupportedEncodingException ignore) {
            }
            return queryString;
        }

        // 处理参数
        private Map<String, String[]> decodeParameter(Map<String, String[]> parameterMap) {
            final Map<String, String[]> filteredParams = new HashMap<String, String[]>();

            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                String key = entry.getKey();
                String[] values = entry.getValue();
                String[] decoded = new String[values.length];
                for (int i = 0; i < values.length; i++) {
                    try {
                        decoded[i] = new String(values[i].getBytes(containerURIEncoding), charset);
                    } catch (UnsupportedEncodingException e) {
                        decoded[i] = values[i];
                    }
                }
                filteredParams.put(key, decoded);
            }
            return Collections.unmodifiableMap(filteredParams);
        }

        @Override
        public String getParameter(String name) {
            String[] values = params.get(name);
            return values != null && values.length > 0 ? values[0] : null;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return params;
        }

        @Override
        public String[] getParameterValues(String name) {
            return params.get(name);
        }
    }
}
